﻿using System;
using System.ComponentModel;
using System.Reflection;
using System.Collections.Generic;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core.Emitters
{
    /// <summary>
    /// Emits a class holding properties.  If the given interface derives from <see cref="INotifyPropertyChanged"/> or
    /// <see cref="INotifyPropertyChanging"/> (or both), the events are fired when properties are set.
    /// </summary>
    public class PropertyHolderEmitter : TypeEmitterBase<PropertyHolderOptions>
    {
        private const string PROPERTY_CHANGING_FIELD = "_changing";
        private const string PROPERTY_CHANGED_FIELD = "_changed";

        private readonly bool _hasChangingInterface;
        private readonly bool _hasChangedInterface;

        /// <summary>Constructor, taking the options in parameter.</summary>
        /// <param name="options"></param>
        protected PropertyHolderEmitter(PropertyHolderOptions options)
            : base(options)
        {
            IList<Type> allInterfaces = TypeHelper.GetAllInterfaces(Options.ExposedInterfaceType, true);

            _hasChangingInterface = allInterfaces.Contains(typeof(INotifyPropertyChanging));
            _hasChangedInterface = allInterfaces.Contains(typeof(INotifyPropertyChanged));
        }

        #region Factory Methods
        /// <summary>Creates a factory of instance of property holder.</summary>
        /// <typeparam name="INTER">Interface type exposing properties.</typeparam>
        /// <returns>A factory of instance of property holder.</returns>
        public static IFactory<INTER> GetFactory<INTER>()
            where INTER : class
        {
            return GetFactory<INTER, object>();
        }

        /// <summary>Creates a factory of instance of property holder with a given base class.</summary>
        /// <typeparam name="INTER">Interface type exposing properties.</typeparam>
        /// <typeparam name="BASE">Type of the base class.</typeparam>
        /// <returns>A factory of instance of property holder.</returns>
        public static IFactory<INTER> GetFactory<INTER, BASE>()
            where INTER : class
            where BASE : class
        {
            PropertyHolderOptions options = new PropertyHolderOptions
            {
                ExposedInterfaceType = typeof(INTER),
                BaseType = typeof(BASE)
            };
            object untypedFactory = GetFactory(options);
            IFactory<INTER> factory = untypedFactory as IFactory<INTER>;

            if (factory == null)
            {
                throw new ApplicationException("Unable to cast the factory to the proper factory type");
            }

            return factory;
        }

        /// <summary>
        /// Creates a factory of instance of property holder with a given base class taking a parameter in construction.
        /// </summary>
        /// <typeparam name="INTER">Interface type exposing properties.</typeparam>
        /// <typeparam name="BASE">Type of the base class.</typeparam>
        /// <typeparam name="BASE_CTOR_PARAM">Type of the base class constructor parameter.</typeparam>
        /// <returns>A factory of instance of property holder.</returns>
        public static IFactory<INTER, BASE_CTOR_PARAM> GetFactory<INTER, BASE, BASE_CTOR_PARAM>()
            where INTER : class
            where BASE : class
        {
            PropertyHolderOptions options = new PropertyHolderOptions
            {
                ExposedInterfaceType = typeof(INTER),
                BaseType = typeof(BASE),
                BaseConstructorParameterTypeList = new Type[] { typeof(BASE_CTOR_PARAM) }
            };
            object untypedFactory = GetFactory(options);
            IFactory<INTER, BASE_CTOR_PARAM> factory = untypedFactory as IFactory<INTER, BASE_CTOR_PARAM>;

            if (factory == null)
            {
                throw new ApplicationException("Unable to cast the factory to the proper factory type");
            }

            return factory;
        }

        /// <summary>Creates a factory of instance of property holder given options.</summary>
        /// <param name="options"></param>
        /// <returns></returns>
        public static object GetFactory(PropertyHolderOptions options)
        {
            if (options == null)
            {
                throw new ArgumentNullException("options");
            }

            PropertyHolderEmitter emitter = new PropertyHolderEmitter(options);
            object factory = options.Cache.GetFactory(emitter);

            return factory;
        }
        #endregion

        /// <summary>Emits a factory corresponding to the state of this emitter.</summary>
        /// <param name="componentTypeName"></param>
        /// <param name="factoryTypeName"></param>
        /// <param name="moduleBuilder"></param>
        /// <returns></returns>
        public override object EmitFactory(
            string componentTypeName, string factoryTypeName, ModuleBuilderExtender moduleBuilder)
        {
            IList<Type> allInterfaces = TypeHelper.GetAllInterfaces(Options.ExposedInterfaceType, true);
            TypeBuilderExtender typeBuilder = moduleBuilder.CreateType(
                componentTypeName,
                Options.BaseType,
                Options.ExposedInterfaceType);
            Type[] baseClassInterfaces = Options.IsUsingBaseClassInterfaces
                ? TypeHelper.GetAllInterfaces(Options.BaseType, true)
                : new Type[0];
            Type[] implementedInterfaces = TypeHelper.Concat(
                new Type[] { typeof(INotifyPropertyChanging), typeof(INotifyPropertyChanged) },
                baseClassInterfaces);

            DefinePrivateFields(typeBuilder, allInterfaces);
            if (_hasChangingInterface)
            {
                typeBuilder.DefinePrivateField(PROPERTY_CHANGING_FIELD, typeof(PropertyChangingEventHandler), false, false);
                ImplementInterface(
                    typeBuilder, typeof(INotifyPropertyChanging), null, null, EmitAddRemoveMethod, EmitAddRemoveMethod, null);
            }
            if (_hasChangedInterface)
            {
                typeBuilder.DefinePrivateField(PROPERTY_CHANGED_FIELD, typeof(PropertyChangedEventHandler), false, false);
                ImplementInterface(
                    typeBuilder, typeof(INotifyPropertyChanged), null, null, EmitAddRemoveMethod, EmitAddRemoveMethod, null);
            }
            ImplementInterface(
                typeBuilder,
                Options.ExposedInterfaceType,
                EmitGetMethod,
                EmitSetMethod,
                null,
                null,
                null,
                implementedInterfaces);
            DefineInstanceConstructor(typeBuilder, Options.BaseType, Options.BaseConstructorParameterTypeList);

            Type componentType = typeBuilder.TypeBuilder.CreateType();
            object factory =
                CreateFactoryInstance(factoryTypeName, componentType, Options.ExposedInterfaceType, moduleBuilder);

            return factory;
        }

        private void DefinePrivateFields(TypeBuilderExtender typeBuilder, IEnumerable<Type> interfaceTypeList)
        {
            foreach (Type interfaceType in interfaceTypeList)
            {
                foreach (PropertyInfo property in interfaceType.GetProperties())
                {
                    typeBuilder.DefinePrivateField(GetFieldName(property), property.PropertyType, false, false);
                }
            }
        }

        private string GetFieldName(PropertyInfo property)
        {
            return string.Concat("_", property.Name);
        }

        private void EmitGetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            ilGenerator.StackField(GetFieldName(property));
            ilGenerator.ReturnStack();
        }

        private void EmitSetMethod(ILGeneratorExtender ilGenerator, MethodInfo method, PropertyInfo property)
        {
            ilGenerator.StackMethodParameter(0);
            if (_hasChangingInterface)
            {
                Label noHandlerLabel = ilGenerator.DefineLabel();

                ilGenerator.StackField(PROPERTY_CHANGING_FIELD);
                ilGenerator.IfNotNull(noHandlerLabel);
                ilGenerator.StackField(PROPERTY_CHANGING_FIELD);
                //  Source of the event:  this
                ilGenerator.StackMethodParameter(0);
                ilGenerator.StackValue(property.Name);
                ilGenerator.StackNew(typeof(PropertyChangingEventArgs).GetConstructor(new Type[] { typeof(string) }));
                ilGenerator.CallMethod(typeof(PropertyChangingEventHandler).GetMethod("Invoke"));
                ilGenerator.MarkLabel(noHandlerLabel);
            }
            ilGenerator.StackMethodParameter(1);
            ilGenerator.StoreStackInField(GetFieldName(property));
            if (_hasChangedInterface)
            {
                Label noHandlerLabel = ilGenerator.DefineLabel();

                ilGenerator.StackField(PROPERTY_CHANGED_FIELD);
                ilGenerator.IfNotNull(noHandlerLabel);
                ilGenerator.StackField(PROPERTY_CHANGED_FIELD);
                //  Source of the event:  this
                ilGenerator.StackMethodParameter(0);
                ilGenerator.StackValue(property.Name);
                ilGenerator.StackNew(typeof(PropertyChangedEventArgs).GetConstructor(new Type[] { typeof(string) }));
                ilGenerator.CallMethod(typeof(PropertyChangedEventHandler).GetMethod("Invoke"));
                ilGenerator.MarkLabel(noHandlerLabel);
            }
            ilGenerator.ReturnStack();
        }

        private void EmitAddRemoveMethod(ILGeneratorExtender ilGenerator, MethodInfo method, EventInfo eventInfo)
        {   //  Switch between the 'changed' and 'changing' events
            string fieldName = eventInfo.DeclaringType == typeof(INotifyPropertyChanged)
                ? PROPERTY_CHANGED_FIELD
                : PROPERTY_CHANGING_FIELD;
            MethodInfo delegateMethod =
                method == eventInfo.GetAddMethod()
                ? typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) })
                : typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) });

            ilGenerator.StackMethodParameter(0);
            ilGenerator.StackField(fieldName);
            ilGenerator.StackMethodParameter(1);
            ilGenerator.CallMethod(delegateMethod);
            ilGenerator.Cast(eventInfo.EventHandlerType);
            ilGenerator.StoreStackInField(fieldName);
            ilGenerator.ReturnStack();
        }
    }
}